<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgpu - volumetric cloud</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>

	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - volumetric cloud
		</div>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.webgpu.js",
					"three/tsl": "../build/three.webgpu.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three';
			import { vec3, materialReference, smoothstep, If, Break, Fn } from 'three/tsl';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';

			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

			let renderer, scene, camera;
			let mesh;

			init();

			function init() {

				renderer = new THREE.WebGPURenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.setAnimationLoop( animate );
				document.body.appendChild( renderer.domElement );

				scene = new THREE.Scene();

				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
				camera.position.set( 0, 0, 1.5 );

				new OrbitControls( camera, renderer.domElement );

				// Sky

				const canvas = document.createElement( 'canvas' );
				canvas.width = 1;
				canvas.height = 32;

				const context = canvas.getContext( '2d' );
				const gradient = context.createLinearGradient( 0, 0, 0, 32 );
				gradient.addColorStop( 0.0, '#014a84' );
				gradient.addColorStop( 0.5, '#0561a0' );
				gradient.addColorStop( 1.0, '#437ab6' );
				context.fillStyle = gradient;
				context.fillRect( 0, 0, 1, 32 );

				const skyMap = new THREE.CanvasTexture( canvas );
				skyMap.colorSpace = THREE.SRGBColorSpace;

				const sky = new THREE.Mesh(
					new THREE.SphereGeometry( 10 ),
					new THREE.MeshBasicNodeMaterial( { map: skyMap, side: THREE.BackSide } )
				);
				scene.add( sky );

				// Texture

				const size = 128;
				const data = new Uint8Array( size * size * size );

				let i = 0;
				const scale = 0.05;
				const perlin = new ImprovedNoise();
				const vector = new THREE.Vector3();

				for ( let z = 0; z < size; z ++ ) {

					for ( let y = 0; y < size; y ++ ) {

						for ( let x = 0; x < size; x ++ ) {

							const d = 1.0 - vector.set( x, y, z ).subScalar( size / 2 ).divideScalar( size ).length();
							data[ i ] = ( 128 + 128 * perlin.noise( x * scale / 1.5, y * scale, z * scale / 1.5 ) ) * d * d;
							i ++;

						}

					}

				}

				const texture = new THREE.Data3DTexture( data, size, size, size );
				texture.format = THREE.RedFormat;
				texture.minFilter = THREE.LinearFilter;
				texture.magFilter = THREE.LinearFilter;
				texture.unpackAlignment = 1;
				texture.needsUpdate = true;


				const geometry = new THREE.BoxGeometry( 1, 1, 1 );

				const material = new THREE.VolumeNodeMaterial( {
					side: THREE.BackSide,
					transparent: true
				} );

				material.map = texture;
				material.base = new THREE.Color( 0x798aa0 );
				material.steps = 100;
				material.range = 0.1;
				material.threshold = 0.25;
				material.opacity = 0.25;

				const range = materialReference( 'range', 'float' );
				const threshold = materialReference( 'threshold', 'float' );
				const opacity = materialReference( 'opacity', 'float' );

				material.testNode = Fn( ( { map, mapValue, probe, finalColor } ) => {

					mapValue.assign( smoothstep( threshold.sub( range ), threshold.add( range ), mapValue ).mul( opacity ) );

					const shading = map.uv( probe.add( vec3( - 0.01 ) ) ).r.sub( map.uv( probe.add( vec3( 0.01 ) ) ).r );

					const col = shading.mul( 3.0 ).add( probe.x.add( probe.y ).mul( 0.25 ) ).add( 0.2 );

					finalColor.rgb.addAssign( finalColor.a.oneMinus().mul( mapValue ).mul( col ) );

					finalColor.a.addAssign( finalColor.a.oneMinus().mul( mapValue ) );

					If( finalColor.a.greaterThanEqual( 0.95 ), () => {

						Break();

					} );

				} );

				mesh = new THREE.Mesh( geometry, material );

				scene.add( mesh );

				//

				const parameters = {
					threshold: 0.25,
					opacity: 0.25,
					range: 0.1,
					steps: 100
				};

				function update() {

					material.threshold = parameters.threshold;
					material.opacity = parameters.opacity;
					material.range = parameters.range;
					material.steps = parameters.steps;

				}

				const gui = new GUI();
				gui.add( parameters, 'threshold', 0, 1, 0.01 ).onChange( update );
				gui.add( parameters, 'opacity', 0, 1, 0.01 ).onChange( update );
				gui.add( parameters, 'range', 0, 1, 0.01 ).onChange( update );
				gui.add( parameters, 'steps', 0, 200, 1 ).onChange( update );

				window.addEventListener( 'resize', onWindowResize );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				mesh.rotation.y = - performance.now() / 7500;

				renderer.render( scene, camera );

			}

		</script>

	</body>
</html>
